# Python3
#
# See the accompanying LICENSE file.
#
# Extracts rst function comments and dumps them to a file

import sys
import os
import re
import urllib.request
import tempfile
import json
import pathlib

if len(sys.argv) != 5:
    print("You must supply sqlite version, docdb filename, input and output filenames", file=sys.stderr)

sqlite_version = sys.argv[1]
docdbfilename = sys.argv[2]
infilename = sys.argv[3]
outfilename = sys.argv[4]

if os.path.exists(outfilename):
    os.remove(outfilename)

basesqurl = "https://sqlite.org/"

op = []
op.append(".. Automatically generated by code2rst.py")
op.append(f"   Edit { infilename } not this file!")
op.append("")
if infilename != "src/apsw.c":
    op.append(".. currentmodule:: apsw")
    op.append("")

import apsw

with tempfile.NamedTemporaryFile() as f:
    f.write(urllib.request.urlopen(basesqurl + "toc.db").read())
    f.flush()

    db = apsw.Connection(f.name)
    db.execute(pathlib.Path(__file__).with_name("tocupdate.sql").read_text())

    funclist = {}
    consts = {}
    const2page = {}

    for name, type, title, uri in db.execute("select name, type, title, uri from toc"):
        if type == "function":
            funclist[name] = basesqurl + uri
        elif type == "constant":
            const2page[name] = basesqurl + uri
            if title not in consts:
                consts[title] = {"vars": []}
            consts[title]["vars"].append(name)
            consts[title]["page"] = basesqurl + (uri.split("#")[0] if "fts5" not in uri else uri)


def do_mappings():
    maps = sorted(mappings.keys())

    seenmappings = set()
    for map in maps:
        # which page does this correspond to?
        m = mappings[map]

        foundin = set()
        for val in m:
            # present in multiple mappings etc
            if val in {"SQLITE_OK", "SQLITE_IGNORE", "SQLITE_ABORT"}:
                continue
            for desc, const in consts.items():
                if val in const["vars"]:
                    foundin.add(desc)

        assert len(foundin) == 1, f"Expected 1 item { foundin } for { m } of { map }"
        desc = list(foundin)[0]
        seenmappings.add(desc)

        # check to see if apsw is missing any
        shouldexit = False
        lookfor = set(consts[desc]["vars"])

        for v in lookfor:
            if v not in mappings[map] and v not in {"SQLITE_TRACE", "SQLITE_CONFIG_ROWID_IN_VIEW"}:
                print("Mapping", map, "is missing", v)
                shouldexit = True
        if shouldexit:
            sys.exit(1)

        op.append(f".. data:: { map }")
        op.append("    :type: dict[str | int, int | str]")
        op.append("")
        op.append("    `" + desc + " <" + consts[desc]["page"] + ">`__ constants")
        op.append("")
        if map.startswith("mapping_session_"):
            op.append("    (Only present when :doc:`session extension <session>` is enabled)")
            op.append("")

        vals = m[:]
        vals.sort()
        op.append("")
        op.append("    %s" % (", ".join(["`%s <%s>`__" % (v, const2page[v]) for v in vals]),))
        op.append("")

    ignores = {
        "Compile-Time Library Version Numbers",
        "Constants Defining Special Destructor Behavior",
        "Fundamental Datatypes",
        "Maximum xShmLock index",
        "Mutex Types",
        "Prepared Statement Scan Status Opcodes",
        "Testing Interface Operation Codes",
        "Text Encodings",
        "Flags for sqlite3_deserialize()",
        "Flags for sqlite3_serialize",
        "Win32 Directory Types",
        "Prepared Statement Scan Status",
    }

    for d in sorted(consts.keys()):
        if d not in seenmappings and d not in ignores:
            print("Missing mapping", d, "with values", consts[d]["vars"], "at", consts[d]["page"])


# we have our own markup to describe what sqlite3 calls we make using
# -* and then a space separated list.  Maybe this could just be
# automatically derived from the source?
def do_calls(line):
    line = line.strip().split()
    assert line[0] == "-*"
    indexop = ["", ".. index:: " + (", ".join(line[1:])), ""]
    saop = ["", "Calls:"]

    calls = []

    for func in line[1:]:
        calls.append("`%s <%s>`__" % (func, funclist[func]))

    if len(calls) == 1:
        saop[-1] = saop[-1] + " " + calls[0]
    else:
        for c in calls:
            saop.append("  * " + c)

    saop.append("")
    return indexop, saop


def do_methods():
    if not methods:
        return
    # special handling for __init__ - add into class body
    try:
        docdb = json.load(open(docdbfilename))
    except Exception:
        docdb = {}
    if not curclass:
        docdb["apsw"] = docdb.get("apsw", {}) | methods
    else:
        docdb[curclass] = methods
    json.dump(docdb, open(docdbfilename, "w"))
    i = "__init__"
    if i in methods:
        v = methods.pop(i)
        dec = v[0]
        p = dec.index(i) + len(i)
        sig = dec[p:]
        body = v[1:]
        indexop, saop = [], []
        newbody = []
        for line in body:
            if line.strip().startswith("-*"):
                indexop, saop = do_calls(line)
            else:
                newbody.append(line)
        body = newbody
        for j in range(-1, -9999, -1):
            if op[j].startswith(".. class::"):
                for l in indexop:
                    op.insert(j, l)
                op[j] = op[j] + sig
                break
        op.append("")
        op.extend(body)
        op.append("")
        op.extend(fixup(op, saop))
        op.append("")

    keys = sorted(methods.keys())

    for k in keys:
        if k.endswith(".<class>"):
            continue
        op.append("")
        d = methods[k]
        dec = d[0]
        d = d[1:]
        indexop = []
        saop = []
        newd = []
        for line in d:
            if line.strip().startswith("-*"):
                indexop, saop = do_calls(line)
            else:
                newd.append(line)

        d = newd

        if curclass and curclass.startswith("VT"):
            for line in d:
                if line.lstrip() != line:
                    indent = line[: len(line) - len(line.lstrip())]
                    break
            method = k
            if k.startswith("Update"):
                method = "Update"
            elif k == "Rollback":
                method = "RollbackTo"
            elif k == "ColumnNoChange":
                method = "Column"
            elif k == "BestIndexObject":
                method = "BestIndex"
            target = f"the_x{ method.lower() }_method"
            if method in {"Savepoint", "Release", "RollbackTo"}:
                target = "the_xsavepoint_xrelease_and_xrollbackto_methods"
            d.extend(("", f"{ indent }`SQLite x{ method } reference <https://sqlite.org/vtab.html#{ target }>`__"))

        # insert index stuff
        op.extend(indexop)
        # we need full buffer name and others
        dec = re.sub(r"\bBuffer\b", "collections.abc.Buffer", dec)
        dec = re.sub(r"\bCallable\b", "typing.Callable", dec)
        dec = re.sub(r"\bAny\b", "typing.Any", dec)
        # insert classname into dec
        if curclass:
            dec = re.sub(r"^(\.\.\s+(method|attribute)::\s+)()", r"\1" + curclass + ".", dec)
        op.append(dec)
        op.extend(d)
        op.append("")
        op.extend(fixup(op, saop))


# op is current output, integrate is unindented lines that need to be
# indented correctly for output
def fixup(op, integrate):
    if len(integrate) == 0:
        return []
    prefix = 999999
    for i in range(-1, -99999, -1):
        if op[i].startswith(".. "):
            break
        if len(op[i].strip()) == 0:
            continue
        leading = len(op[i]) - len(op[i].lstrip())
        prefix = min(prefix, leading)
    return [" " * prefix + line for line in integrate]


methods = {}

curop = []

cursection = None

incomment = False
curclass = None

if infilename == "src/apsw.c":
    # this stuff used to be in apsw.c but moved to constants.c
    # which is generated from toc.db so they can't really get
    # out of sync, but remain paranoid
    mappings = {}
    mappingre = re.compile(r""".*, "(mapping_\w+)",.*""")
    constre = re.compile(r"""\s+"(\w+)",\s*\1,.*""")
    constitems = []
    for line in open("src/constants.c", "rt"):
        m = constre.match(line)
        if m:
            constitems.append(m.group(1))
        m = mappingre.match(line)
        if m:
            mappings[m.group(1)] = constitems
            constitems = []
    assert not constitems
else:
    mappings = None

for line in open(infilename, "rt"):
    line = line.rstrip()

    if not incomment and line.lstrip().startswith("/**"):
        # a comment intended for us
        line = line.lstrip(" \t/*")
        cursection = line
        incomment = True
        assert len(curop) == 0
        if len(line):
            t = line.split()[1]
            if t == "class::":
                if methods:
                    do_methods()
                    methods = {f"{ line.split()[-1] }.<class>": curop}
                curclass = line.split()[2].split("(")[0]
        curop.append(line)
        continue
    # end of comment
    if incomment and line.lstrip().startswith("*/"):
        op.append("")
        incomment = False
        line = cursection
        if len(line):
            t = cursection.split()[1]
            if t in ("automethod::", "method::", "attribute::", "data::"):
                name = line.split()[2].split("(")[0]
                methods[name] = curop
            elif t == "class::":
                methods = {f"{ line.split()[-1] }.<class>": curop}
                op.append("")
                op.append(curclass + " class")
                op.append("=" * len(op[-1]))
                op.append("")
                op.extend(curop)
            # I keep forgetting double colons
            elif t.endswith(":") and not t.endswith("::"):
                raise Exception("You forgot double colons: " + line)
            else:
                if methods:
                    import pdb

                    pdb.set_trace()
                assert not methods  # check no outstanding methods
                op.extend(curop)
        else:
            do_methods()
            methods = {}
            op.extend(curop)
        curop = []
        continue
    # ordinary comment line
    if incomment:
        curop.append(line)
        continue

    # ignore everything else

if methods:
    do_methods()

if mappings:
    do_mappings()

# remove double blank lines
op2 = []
for i in range(len(op)):
    if i + 1 < len(op) and len(op[i].strip()) == 0 and len(op[i + 1].strip()) == 0:
        continue
    if len(op[i].strip()) == 0:
        op2.append("")
    else:
        op2.append(op[i].rstrip())
op = op2

pathlib.Path(outfilename).write_text("\n".join(op) + "\n")
